/*
 * Copyright (c) 2010. Dylan Schell.
 */

package nl.xs4all.dylanschell.deckprinter.services;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import nl.xs4all.dylanschell.deckprinter.model.CardInDeck;
import nl.xs4all.dylanschell.deckprinter.model.CardInSet;
import nl.xs4all.dylanschell.deckprinter.model.SetDescriptor;
import nl.xs4all.dylanschell.deckprinter.util.Distance;

import java.io.File;
import java.io.FilenameFilter;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

@Singleton
public class SetServiceImpl implements SetService
{
    private final Map<UUID, SetDescriptor> sets;
    private final DeckPrinterConfig config;
    private final SetLoader setLoader;
    private ExecutorService executorService;
    private Distance distance = new Distance();

    @Inject
    public SetServiceImpl(final SetLoader setLoader,
            DeckPrinterConfig config, ExecutorService executorService)
    {
        this.config = config;
        this.setLoader = setLoader;
        this.executorService = executorService;
        this.sets = initialize();
    }

    private Map<UUID, SetDescriptor> initialize()
    {
        final Map<UUID, SetDescriptor> sets = new ConcurrentHashMap<UUID, SetDescriptor>();
        File setFolder = config.getSetFolder();
        File[] setFiles = setFolder.listFiles(new FilenameFilter()
        {
            public boolean accept(File file, String s)
            {
                return s.endsWith(".o8s");
            }
        });
        CompletionService<Boolean> cs = new ExecutorCompletionService<Boolean>(executorService);
        for (final File f : setFiles)
        {
            Callable<Boolean> c = new Callable<Boolean>()
            {
                public Boolean call()
                {
                    try
                    {
                        SetDescriptor set = setLoader.loadSet(f);
                        sets.put(set.getId(), set);
                        System.out.println("Loaded set: " + set.getName());
                    } catch (Exception e)
                    {
                        return false;
                    }
                    return true;
                }
            };
            cs.submit(c);
        }
        for (final File f : setFiles)
        {
            try
            {
                Boolean result = cs.take().get();
            } catch (InterruptedException e)
            {
                e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
            } catch (ExecutionException e)
            {
                e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
            }
        }
        return sets;
    }

    private Map<UUID, SetDescriptor> getSets()
    {
        return sets;
    }

    public List<SetDescriptor> getAllSets()
    {
        return new ArrayList<SetDescriptor>(getSets().values());
    }

    public byte[] getImage(CardInSet card)
    {
        if (card == null)
        {
            throw new NullPointerException("card is null");
        }
        SetDescriptor sd = getSets().get(card.getSetId());
        if (sd != null)
        {
            String target = sd.getImage(card);
            if (target != null)
            {
                File sourceFile = sd.getSourceFile();
                try
                {
                    ZipFile zf = new ZipFile(sourceFile);
                    try
                    {
                        ZipEntry entry = getEntry(zf, target);
                        if (entry != null)
                        {
                            InputStream is = zf.getInputStream(entry);
                            try
                            {
                                byte[] result = new byte[(int) entry.getSize()];
                                int offset = 0;
                                int remaining = (int) entry.getSize();
                                while (remaining > 0)
                                {
                                    int nRead = is.read(result, offset, remaining);
                                    remaining -= nRead;
                                    offset += nRead;
                                }
                                return result;
                            } finally
                            {
                                is.close();
                            }
                        }
                    } finally
                    {
                        zf.close();
                    }
                } catch (Exception e)
                {
                    e.printStackTrace();
                }
            }
        }
        return new byte[0];
    }

    private ZipEntry getEntry(ZipFile zf, String target)
    {
        ZipEntry result = zf.getEntry(target.substring(1));
        if (result == null)
        {
            String match = target.substring(1).toLowerCase();
            // *SIGH* some zips contain refs in the wrong case..., try to locate it the slow way
            Enumeration<? extends ZipEntry> enumeration = zf.entries();
            while (enumeration.hasMoreElements())
            {
                ZipEntry ze = enumeration.nextElement();
                if (ze.getName().toLowerCase().equals(match))
                {
                    result = zf.getEntry(ze.getName());
                    break;
                }
            }
        }
        return result;
    }

    public byte[] getImage(CardInDeck card)
    {
        CardInSet cis = getCardInSet(card);
        if (cis != null)
        {
            return getImage(cis);
        }
        return null;
    }

    public CardInSet getCardInSet(final CardInDeck card)
    {
        for (SetDescriptor sd : getAllSets())
        {
            CardInSet cis = sd.getCard(card);
            if (cis != null)
            {
                return cis;
            }
        }
        CardInSet bestMatch = null;
        int bestDistance = Integer.MAX_VALUE;
        CompletionService<CardInSet> cs = new ExecutorCompletionService<CardInSet>(executorService);
        for (final SetDescriptor sd : getAllSets())
        {
            cs.submit(new Callable<CardInSet>()
            {
                @Override
                public CardInSet call() throws Exception
                {
                    return sd.getCardByName(card.getName(), true);
                }
            });
        }
        for (final SetDescriptor sd : getAllSets())
        {
            try
            {
                CardInSet cis = cs.take().get();
                if (cis != null)
                {
                    int d = distance.LD(card.getName(), cis.getName());
                    if (d < bestDistance)
                    {
                        bestMatch = cis;
                        bestDistance = d;
                    }
                }
            } catch (InterruptedException e)
            {
                e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
            } catch (ExecutionException e)
            {
                e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
            }
        }
        if (bestDistance < 4)
        {
            return bestMatch;
        }
        return null;
    }

    public UUID getSetId(String setName)
    {
        for (SetDescriptor sd : getSets().values())
        {
            if (sd.getName().equals(setName))
            {
                return sd.getId();
            }
        }
        return null;
    }

    public long getLastModified(CardInSet cis)
    {
        SetDescriptor sd = getSets().get(cis.getSetId());
        return sd.getSourceFile().lastModified();
    }
}
